package org.scs.carleton.ca.pgdt.core.ui.internal.highlighting;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextPresentationListener;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.swt.custom.StyleRange;
import org.scs.carleton.ca.pgdt.core.ui.internal.highlighting.PrologHighlightingManager.HighlightedPosition;
import org.scs.carleton.ca.pgdt.core.ui.internal.highlighting.PrologHighlightingManager.HighlightingStyle;

/**
 * 
 * UI Thread implementation of the highlighter. Modelled after the SemanticHighlightingPresenter in JDT
 * 
 * @author tessierj
 * 
 */
public class PrologHighlightingPresenter implements ITextPresentationListener, ITextInputListener, IDocumentListener {
	private boolean isCanceled = false;

	/** Lock for positions */
	private Object positionLock = new Object();
	/** UI's current highlighted positions - can contain <code>null</code> elements */
	private List<HighlightedPosition> positions;
	/** Position updater */
	private IPositionUpdater positionUpdater;
	/** Source viewer */
	private ISourceViewer sourceViewer;
	/** Presentation reconciler */
	private PrologPresentationReconciler presentationReconciler;

	public PrologHighlightingPresenter() {
		sourceViewer = null;
		positionUpdater = new HighlightingPositionUpdater(getPositionCategory());
		positions = new ArrayList<HighlightedPosition>(5);
	}

	public void install(ISourceViewer sourceViewer, PrologPresentationReconciler presentationReconciler) {
		this.sourceViewer = sourceViewer;
		this.presentationReconciler = presentationReconciler;
		if (sourceViewer != null) {
			((SourceViewer) sourceViewer).addTextPresentationListener(this);
			this.sourceViewer.addTextInputListener(this);
			manageDocument(sourceViewer.getDocument());
		}
	}

	private void insertPosition(HighlightedPosition position) {
		int i = computeIndexAfterOffset(positions, position.getOffset());
		positions.add(i, position);
	}

	/**
	 * Add a position with the given range and highlighting unconditionally, only from UI thread. The position will also be registered on the document. The text presentation is not invalidated.
	 * 
	 * @param offset
	 *            The range offset
	 * @param length
	 *            The range length
	 * @param highlighting
	 */
	private void addPositionFromUI(int offset, int length, HighlightingStyle highlighting) {
		HighlightedPosition position = createHighlightedPosition(offset, length, highlighting);
		synchronized (positionLock) {
			insertPosition(position);
		}

		IDocument document = sourceViewer.getDocument();
		if (document == null)
			return;
		String positionCategory = getPositionCategory();
		try {
			document.addPosition(positionCategory, position);
		}
		catch (BadLocationException e) {
			// Should not happen
		}
		catch (BadPositionCategoryException e) {
			// Should not happen
		}
	}

	@Override
	public void applyTextPresentation(TextPresentation textPresentation) {
		IRegion region = textPresentation.getExtent();
		int i = computeIndexAtOffset(positions, region.getOffset()), n = computeIndexAtOffset(positions, region.getOffset() + region.getLength());
		if (n - i > 2) {
			List<StyleRange> ranges = new ArrayList<StyleRange>(n - i);
			for (; i < n; i++) {
				HighlightedPosition position = positions.get(i);
				if (!position.isDeleted())
					ranges.add(position.createStyleRange());
			}
			StyleRange[] array = new StyleRange[ranges.size()];
			array = ranges.toArray(array);
			textPresentation.replaceStyleRanges(array);
		} else {
			for (; i < n; i++) {
				HighlightedPosition position = positions.get(i);
				if (!position.isDeleted())
					textPresentation.replaceStyleRange(position.createStyleRange());
			}
		}
	}

	/**
	 * Returns the index of the first position with an offset greater than the given offset.
	 * 
	 * @param positions
	 *            the positions, must be ordered by offset and must not overlap
	 * @param offset
	 *            the offset
	 * @return the index of the last position with an offset greater than the given offset
	 */
	private int computeIndexAfterOffset(List<HighlightedPosition> positions, int offset) {
		int i = -1;
		int j = positions.size();
		while (j - i > 1) {
			int k = (i + j) >> 1;
			Position position = positions.get(k);
			if (position.getOffset() > offset)
				j = k;
			else
				i = k;
		}
		return j;
	}

	/**
	 * Returns the index of the first position with an offset equal or greater than the given offset.
	 * 
	 * @param positions
	 *            the positions, must be ordered by offset and must not overlap
	 * @param offset
	 *            the offset
	 * @return the index of the last position with an offset equal or greater than the given offset
	 */
	private int computeIndexAtOffset(List<HighlightedPosition> positions, int offset) {
		int i = -1;
		int j = positions.size();
		while (j - i > 1) {
			int k = (i + j) >> 1;
			Position position = positions.get(k);
			if (position.getOffset() >= offset)
				j = k;
			else
				i = k;
		}
		return j;
	}

	/**
	 * Creates a highlighted position at a specific location in the document
	 * 
	 * @param offset
	 * @param length
	 * @param highlighting
	 * @return
	 */
	HighlightedPosition createHighlightedPosition(int offset, int length, HighlightingStyle highlighting) {
		return new HighlightedPosition(offset, length, highlighting, positionUpdater);
	}

	public TextPresentation createPresentation(List<HighlightedPosition> addedPositions, List<HighlightedPosition> removedPositions) {
		if (!(sourceViewer == null || presentationReconciler == null || isCanceled())) {
			IDocument document = sourceViewer.getDocument();
			if (document != null) {
				int minStart = Integer.MAX_VALUE;
				int maxEnd = Integer.MIN_VALUE;

				for (int i = 0, n = removedPositions.size(); i < n; i++) {
					Position position = removedPositions.get(i);
					int offset = position.getOffset();
					minStart = Math.min(minStart, offset);
					maxEnd = Math.max(maxEnd, offset + position.getLength());
				}
				
				for (int i = 0, n = addedPositions.size(); i < n; i++) {
					Position position = addedPositions.get(i);
					int offset = position.getOffset();
					minStart = Math.min(minStart, offset);
					maxEnd = Math.max(maxEnd, offset + position.getLength());
				}

				if (minStart < maxEnd) {
					try {
						return presentationReconciler.createRepairDescription(new Region(minStart, maxEnd - minStart), document);
					}
					catch (RuntimeException e) {
						// Assume concurrent modification from UI thread
					}
				}
			}
		}

		return null;
	}

	@Override
	public void documentAboutToBeChanged(DocumentEvent event) {
		setCanceled(true);
	}

	@Override
	public void documentChanged(DocumentEvent event) {

	}

	/**
	 * @param document
	 *            the document
	 * @return the document's lock object
	 */
	private Object getLockObject(IDocument document) {
		if (document instanceof ISynchronizable) {
			Object lock = ((ISynchronizable) document).getLockObject();
			if (lock != null)
				return lock;
		}
		return document;
	}

	/**
	 * @return The semantic reconciler position's category.
	 */
	private String getPositionCategory() {
		return toString();
	}

	@Override
	public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
		setCanceled(true);
		releaseDocument(oldInput);
		resetState();
	}

	@Override
	public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
		manageDocument(newInput);
	}

	/**
	 * Start managing the given document.
	 * 
	 * @param document
	 *            The document
	 */
	private void manageDocument(IDocument document) {
		if (document != null) {
			document.addPositionCategory(getPositionCategory());
			document.addPositionUpdater(positionUpdater);
			document.addDocumentListener(this);
		}
	}

	/**
	 * Stop managing the given document.
	 * 
	 * @param document
	 *            The document
	 */
	private void releaseDocument(IDocument document) {
		if (document != null) {
			document.removeDocumentListener(this);
			document.removePositionUpdater(positionUpdater);
			try {
				document.removePositionCategory(getPositionCategory());
			} catch (BadPositionCategoryException e) {
				// Should not happen
			}
		}
	}

	/**
	 * Reset to initial state.
	 */
	private void resetState() {
		synchronized (positionLock) {
			positions.clear();
		}
	}

	/**
	 * Set whether or not the current reconcile is canceled.
	 * 
	 * @param isCanceled
	 *            <code>true</code> iff the current reconcile is canceled
	 */
	public void setCanceled(boolean isCanceled) {
		IDocument document = sourceViewer != null ? sourceViewer.getDocument() : null;
		if (document == null) {
			this.isCanceled = isCanceled;
		} else {
			synchronized (getLockObject(document)) {
				this.isCanceled = isCanceled;
			}
		}
	}

	/**
	 * @return Returns <code>true</code> iff the current reconcile is canceled.
	 *         <p>
	 *         NOTE: Also called from background thread.
	 *         </p>
	 */
	public boolean isCanceled() {
		IDocument document = sourceViewer != null ? sourceViewer.getDocument() : null;

		if (document == null) {
			return isCanceled;
		} else {
			synchronized (getLockObject(document)) {
				return isCanceled;
			}
		}
	}

	/**
	 * Semantic highlighting position updater. Shamelessly stolen from JDT
	 */
	private class HighlightingPositionUpdater implements IPositionUpdater {

		/** The position category. */
		private final String CATEGORY;

		/**
		 * Creates a new updater for the given <code>category</code>.
		 * 
		 * @param category
		 *            the new category.
		 */
		public HighlightingPositionUpdater(String category) {
			this.CATEGORY = category;
		}

		/*
		 * @see org.eclipse.jface.text.IPositionUpdater#update(org.eclipse.jface.text.DocumentEvent)
		 */
		public void update(DocumentEvent event) {

			int eventOffset = event.getOffset();
			int eventOldLength = event.getLength();
			int eventEnd = eventOffset + eventOldLength;

			try {
				Position[] positions = event.getDocument().getPositions(CATEGORY);

				for (int i = 0; i != positions.length; i++) {

					HighlightedPosition position = (HighlightedPosition) positions[i];

					// Also update deleted positions because they get deleted by the background thread and removed/invalidated only in the UI runnable
					// if (position.isDeleted())
					// continue;

					int offset = position.getOffset();
					int length = position.getLength();
					int end = offset + length;

					if (offset > eventEnd)
						updateWithPrecedingEvent(position, event);
					else if (end < eventOffset)
						updateWithSucceedingEvent(position, event);
					else if (offset <= eventOffset && end >= eventEnd)
						updateWithIncludedEvent(position, event);
					else if (offset <= eventOffset)
						updateWithOverEndEvent(position, event);
					else if (end >= eventEnd)
						updateWithOverStartEvent(position, event);
					else
						updateWithIncludingEvent(position, event);
				}
			} catch (BadPositionCategoryException e) {
				// ignore and return
			}
		}

		/**
		 * Update the given position with the given event. The event is included by the position.
		 * 
		 * @param position
		 *            The position
		 * @param event
		 *            The event
		 */
		private void updateWithIncludedEvent(HighlightedPosition position, DocumentEvent event) {
			int eventOffset = event.getOffset();
			String newText = event.getText();
			if (newText == null)
				newText = ""; //$NON-NLS-1$
			int eventNewLength = newText.length();

			int deltaLength = eventNewLength - event.getLength();

			int offset = position.getOffset();
			int length = position.getLength();
			int end = offset + length;

			int includedLength = 0;
			while (includedLength < eventNewLength && Character.isJavaIdentifierPart(newText.charAt(includedLength)))
				includedLength++;
			if (includedLength == eventNewLength)
				position.setLength(length + deltaLength);
			else {
				int newLeftLength = eventOffset - offset + includedLength;

				int excludedLength = eventNewLength;
				while (excludedLength > 0 && Character.isJavaIdentifierPart(newText.charAt(excludedLength - 1)))
					excludedLength--;
				int newRightOffset = eventOffset + excludedLength;
				int newRightLength = end + deltaLength - newRightOffset;

				if (newRightLength == 0) {
					position.setLength(newLeftLength);
				} else {
					if (newLeftLength == 0) {
						position.update(newRightOffset, newRightLength);
					} else {
						position.setLength(newLeftLength);
						addPositionFromUI(newRightOffset, newRightLength, position.getHighlighting());
					}
				}
			}
		}

		/**
		 * Update the given position with the given event. The event includes the position.
		 * 
		 * @param position
		 *            The position
		 * @param event
		 *            The event
		 */
		private void updateWithIncludingEvent(HighlightedPosition position, DocumentEvent event) {
			position.delete();
			position.update(event.getOffset(), 0);
		}

		/**
		 * Update the given position with the given event. The event overlaps with the end of the position.
		 * 
		 * @param position
		 *            The position
		 * @param event
		 *            The event
		 */
		private void updateWithOverEndEvent(HighlightedPosition position, DocumentEvent event) {
			String newText = event.getText();
			if (newText == null)
				newText = ""; //$NON-NLS-1$
			int eventNewLength = newText.length();

			int includedLength = 0;
			while (includedLength < eventNewLength && Character.isJavaIdentifierPart(newText.charAt(includedLength)))
				includedLength++;
			position.setLength(event.getOffset() - position.getOffset() + includedLength);
		}

		/**
		 * Update the given position with the given event. The event overlaps with the start of the position.
		 * 
		 * @param position
		 *            The position
		 * @param event
		 *            The event
		 */
		private void updateWithOverStartEvent(HighlightedPosition position, DocumentEvent event) {
			int eventOffset = event.getOffset();
			int eventEnd = eventOffset + event.getLength();

			String newText = event.getText();
			if (newText == null)
				newText = ""; //$NON-NLS-1$
			int eventNewLength = newText.length();

			int excludedLength = eventNewLength;
			while (excludedLength > 0 && Character.isJavaIdentifierPart(newText.charAt(excludedLength - 1)))
				excludedLength--;
			int deleted = eventEnd - position.getOffset();
			int inserted = eventNewLength - excludedLength;
			position.update(eventOffset + excludedLength, position.getLength() - deleted + inserted);
		}

		/**
		 * Update the given position with the given event. The event precedes the position.
		 * 
		 * @param position
		 *            The position
		 * @param event
		 *            The event
		 */
		private void updateWithPrecedingEvent(HighlightedPosition position, DocumentEvent event) {
			String newText = event.getText();
			int eventNewLength = newText != null ? newText.length() : 0;
			int deltaLength = eventNewLength - event.getLength();

			position.setOffset(position.getOffset() + deltaLength);
		}

		/**
		 * Update the given position with the given event. The event succeeds the position.
		 * 
		 * @param position
		 *            The position
		 * @param event
		 *            The event
		 */
		private void updateWithSucceedingEvent(HighlightedPosition position, DocumentEvent event) {

		}
	}

	public void addAllPositions(List<HighlightedPosition> positions) {
		synchronized (positionLock) {
			this.positions.addAll(positions);
		}
	}

	private int indexOf(List<HighlightedPosition> positions, Position position) {
		int index = computeIndexAtOffset(positions, position.getOffset());
		int size = positions.size();
		while (index < size) {
			if (positions.get(index) == position)
				return index;
			index++;
		}
		return -1;
	}

	/**
	 * Returns <code>true</code> iff the positions contain the position.
	 * 
	 * @param positions
	 *            the positions, must be ordered by offset but may overlap
	 * @param position
	 *            the position
	 * @return <code>true</code> iff the positions contain the position
	 */
	private boolean contain(List<HighlightedPosition> positions, Position position) {
		return indexOf(positions, position) != -1;
	}

	public void updatePresentation(TextPresentation textPresentation, HighlightedPosition[] addedPositions, HighlightedPosition[] removedPositions) {
		if (sourceViewer == null)
			return;

		if (isCanceled())
			return;

		IDocument document = sourceViewer.getDocument();
		if (document == null)
			return;

		String positionCategory = getPositionCategory();

		List<HighlightedPosition> removedPositionsList = Arrays.asList(removedPositions);

		try {
			synchronized (positionLock) {
				List<HighlightedPosition> oldPositions = positions;
				int newSize = Math.max(positions.size() + addedPositions.length - removedPositions.length, 10);

				/*
				 * The following loop is a kind of merge sort: it merges two List<Position>, each sorted by position.offset, into one new list. The first of the two is the previous list of positions (oldPositions), from which any deleted positions get removed on the fly. The second of two is the list of added positions. The result is stored in newPositions.
				 */
				List<HighlightedPosition> newPositions = new ArrayList<HighlightedPosition>(newSize);
				HighlightedPosition position = null;
				HighlightedPosition addedPosition = null;
				for (int i = 0, j = 0, n = oldPositions.size(), m = addedPositions.length; i < n || position != null || j < m || addedPosition != null;) {
					// loop variant: i + j < old(i + j)

					// a) find the next non-deleted Position from the old list
					while (position == null && i < n) {
						position = oldPositions.get(i++);
						if (position.isDeleted() || contain(removedPositionsList, position)) {
							document.removePosition(positionCategory, position);
							position = null;
						}
					}

					// b) find the next Position from the added list
					if (addedPosition == null && j < m) {
						addedPosition = addedPositions[j++];
						document.addPosition(positionCategory, addedPosition);
					}

					// c) merge: add the next of position/addedPosition with the lower offset
					if (position != null) {
						if (addedPosition != null)
							if (position.getOffset() <= addedPosition.getOffset()) {
								newPositions.add(position);
								position = null;
							} else {
								newPositions.add(addedPosition);
								addedPosition = null;
							}
						else {
							newPositions.add(position);
							position = null;
						}
					} else if (addedPosition != null) {
						newPositions.add(addedPosition);
						addedPosition = null;
					}
				}
				positions = newPositions;
			}
		}
		catch (BadPositionCategoryException e) {
			// Should not happen
			throw new RuntimeException(e);
		}
		catch (BadLocationException e) {
			// Should not happen
			throw new RuntimeException(e);
		}

		if (textPresentation != null)
			sourceViewer.changeTextPresentation(textPresentation, false);
		else
			sourceViewer.invalidateTextPresentation();
	}

	public Runnable createUpdateRunnable(final TextPresentation textPresentation, List<HighlightedPosition> addedPositions, List<HighlightedPosition> removedPositions) {
		if (textPresentation == null || sourceViewer == null) {
			return null;
		}

		final HighlightedPosition[] added = new HighlightedPosition[addedPositions.size()];
		addedPositions.toArray(added);
		final HighlightedPosition[] removed = new HighlightedPosition[removedPositions.size()];
		removedPositions.toArray(removed);

		if (isCanceled())
			return null;

		Runnable runnable = new Runnable() {
			public void run() {
				updatePresentation(textPresentation, added, removed);
			}
		};

		return runnable;
	}
}
